﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Windows.Markup;
using System.Xaml;
using System.Xaml.Schema;
using Microsoft.Xaml.Tools.XamlDom;
using System.Collections.ObjectModel;

namespace BindingFinder
{
    public class Binding : BindingData
    {
        public Binding()
        {
        }

        public Binding(string path)
        {
            Path = path;
        }
        [DefaultValue(null)]
        public RelativeSource RelativeSource { get; set; }
        [DefaultValue(null)]
        [ConstructorArgument("path")]
        public string Path { get; set; }
        [DefaultValue(null)]
        public string ElementName { get; set; }
        [DefaultValue(null)]
        public StaticResourceExtension Source { get; set; }
        [DefaultValue(null)]
        public object Converter { get; set; }
        [DefaultValue(null)]
        public string Mode { get; set; }
        [DefaultValue(null)]
        public object FallbackValue { get; set; }
        [DefaultValue(null)]
        public string UpdateSourceTrigger { get; set; }
        [DefaultValue(false)]
        public bool ValidatesOnDataErrors { get; set; }
        [DefaultValue(null)]
        public string StringFormat { get; set; }

        //Added to have complete set for WPF. Haven't taught binding validator to respect these.
        [DefaultValue((string)null)]
        public object AsyncState { get; set; }
        [DefaultValue(false)]
        public bool BindsDirectlyToSource { get; set; }
        [DefaultValue((string)null)]
        public string ConverterCulture { get; set; }
        [DefaultValue((string)null)]
        public object ConverterParameter { get; set; }
        [DefaultValue(false)]
        public bool IsAsync { get; set; }
        [DefaultValue(false)]
        public bool NotifyOnSourceUpdated { get; set; }
        [DefaultValue(false)]
        public bool NotifyOnTargetUpdated { get; set; }
        [DefaultValue(false)]
        public bool NotifyOnValidationError { get; set; }
        [DefaultValue(false)]
        public bool ValidatesOnExceptions { get; set; }
        private Type NamedElementItemType { get; set; }

        private Collection<object> _ValidationRules;
        public Collection<object> ValidationRules
        {
            get
            {
                if (_ValidationRules == null)
                {
                    _ValidationRules = new Collection<object>();
                }
                return _ValidationRules;
            }
        }
        [DefaultValue((string)null)]
        public string XPath { get; set; }

        private XamlType _TypeContext;
        public override XamlType TypeContext
        {
            get
            {
                if (_TypeContext != null)
                    return _TypeContext;
                else
                {
                    if (ElementName != null)
                    {
                        //TODO: binding will actually continue to search for more namescopes above this first namescope...
                        XamlDomObject nearestNamescope = DomObject.GetNamescopeFor();
                        XamlDomObject namedObject = nearestNamescope.FindNamedObject(ElementName);
                        if (namedObject != null)
                        {
                            namedObject.EnsureItemType();
                            NamedElementItemType = XamlDomUtilities.GetItemType(namedObject);
                            _TypeContext = namedObject.GetXamlTypeConsideringXClass();
                            if (_TypeContext == null)
                            {
                                _TypeContext = namedObject.Type;
                            }
                            if (_TypeContext.UnderlyingType == null)
                            {
                                _TypeContext = Program.SchemaContext.GetXamlType(
                                    XamlDomUtilities.GetXamlTypeNameFromLocalAssembly(_TypeContext.PreferredXamlNamespace, _TypeContext.Name)
                                    );
                            }
                        }
                        if (_TypeContext == null)
                        {
                            string messageString = ElementNameNotFound;
                            messageString = string.Format(messageString, this.ElementName);
                            if (!Messages.ContainsKey(messageString))
                            {
                                Messages.Add(messageString, DomObject);
                            }
                        }
                    }
                    else if (RelativeSource != null)
                    {
                        switch (RelativeSource.Mode)
                        {
                            case "FindAncestor":
                                int count = RelativeSource.AncestorLevel;
                                if (count < 1)
                                    count = 1;
                                XamlDomObject domObjectStart = DomObject;
                                XamlDomObject domObjectResult = null;
                                XamlType typeToFind = DomObject.GetXamlTypeFromTypeNameWithPrefix(RelativeSource.AncestorType);
                                while (count > 0)
                                {
                                    domObjectResult = domObjectStart.FindParentObjectByType(typeToFind, false, /*acceptAssignableTypes*/true);
                                    count--;
                                    if (count > 0 && domObjectResult.Parent == null)
                                    {
                                        string messageString = RelativeSourceFindAncestorFailed;
                                        if (!Messages.ContainsKey(messageString))
                                        {
                                            Messages.Add(messageString, DomObject);
                                        }
                                        return null;
                                    }

                                    domObjectStart = domObjectResult;
                                }

                                if (domObjectResult != null)
                                {
                                    _TypeContext = domObjectResult.Type;
                                }
                                else
                                {
                                    string messageString = RelativeSourceFindAncestorFailed;
                                    if (!Messages.ContainsKey(messageString))
                                    {
                                        Messages.Add(messageString, DomObject);
                                    }
                                }
                                break;
                            case "TemplatedParent":
                                XamlDomObject controlTemplateDom = DomObject.FindParentObjectByType(Program.controlTemplateXT, false, /*acceptAssignableTypes*/true);
                                XamlDomObject styleDom = controlTemplateDom.FindParentObjectByType(Program.styleXT, false, /*acceptAssignableTypes*/true);
                                string targetType = styleDom.GetMemberValueOfTypeProperty("TargetType");
                                _TypeContext = styleDom.GetXamlTypeFromTypeNameWithPrefix(targetType);
                                if (_TypeContext == null)
                                {
                                    string messageString = RelativeSourceTemplatedParentFailed;
                                    if (!Messages.ContainsKey(messageString))
                                    {
                                        Messages.Add(messageString, DomObject);
                                    }
                                }
                                break;
                            case "Self":
                                XamlDomObject self = DomObject.Parent.Parent;
                                _TypeContext = self.Type;
                                break;
                            default:
                                //PreviousData is the only .NET enum value not yet implemented.
                                throw new NotImplementedException();
                        }
                    }
                    else if (Source != null)
                    {
                        //TODO: search app.xaml
                        XamlDomObject resourceDom = DomObject.FindResource(Source.ResourceKey);
                        if (resourceDom != null)
                        {
                            if (resourceDom.Type == Program.objectDataProviderXT)
                            {
                                string objectTypeString = resourceDom.GetMemberValueOfTypeProperty("ObjectType");
                                XamlType objectXT = resourceDom.GetXamlTypeFromTypeNameWithPrefix(objectTypeString);
                                Type objectType = objectXT.UnderlyingType;

                                string methodName = resourceDom.GetMemberValue("MethodName");
                                MethodInfo methodInfo = objectType.GetMethod(methodName);
                                _TypeContext = Program.SchemaContext.GetXamlType(methodInfo.ReturnType);
                            }
                            else
                            {
                                _TypeContext = resourceDom.Type;
                            }
                        }
                        if (_TypeContext == null)
                        {
                            string messageString = BindingSourceNotFound;
                            if (!Messages.ContainsKey(messageString))
                            {
                                Messages.Add(messageString, DomObject);
                            }
                        }
                    }
                    else
                    {
                        if (DomObject.Parent != null && DomObject.Parent.Parent.Type.Name == "DataGridTextColumn")
                        {
                            XamlDomObject dataGrid = DomObject.Parent.Parent.Parent.Parent.Parent.Parent;
                            Type itemType = XamlDomUtilities.GetItemType(dataGrid);
                            if (itemType != null)
                            {
                                _TypeContext = Program.SchemaContext.GetXamlType(itemType);
                                if (_TypeContext == null)
                                {
                                    dataGrid.EnsureItemType();
                                    itemType = XamlDomUtilities.GetItemType(dataGrid);
                                    _TypeContext = Program.SchemaContext.GetXamlType(itemType);
                                }
                            }
                        }
                        else
                        {
                            _TypeContext = FindTypeContextInDom(DomObject);
                            if (_TypeContext == null)
                            {
                                string messageString = DDesignContextNeeded;
                                if (!Messages.ContainsKey(messageString)
                                    && !Messages.ContainsKey(DataTemplateDataTypeNeeded))
                                {
                                    Messages.Add(messageString, DomObject);
                                }
                            }
                        }
                    }
                    return _TypeContext;
                }
            }
        }


        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return this;
        }

        private XamlType FindTypeContextInDom(XamlDomObject objectNode)
        {
            XamlDomMember designDataContextMember = null;
            //first check to see if d:DataContext is set
            foreach (XamlDomMember member in objectNode.MemberNodes)
            {
                if (member.Member.Name == "DataContext")
                {
                    designDataContextMember = member;
                    break;
                }
            }
            if (designDataContextMember != null)
            {
                XamlDomObject designInstanceME = ((XamlDomObject)designDataContextMember.Item);
                XamlDomValue value = ((XamlDomValue)designInstanceME.GetPositionalParameter(0));
                XamlType xamlType = null;
                //TODO: need to support DataContext="{Binding}" like Family.Show's MainWindow.xaml line 145.
                //    null check works around issue for now.
                if (value != null)
                {
                    xamlType = designInstanceME.GetXamlTypeFromTypeNameString(value.Value);
                }
                return xamlType;
            }
            else
            {
                //if not then walk up for a DataTemplate
                XamlDomMember parentMember = objectNode.Parent;
                if (parentMember == null)
                {
                    return null;
                }
                else
                {
                    XamlDomObject parentObject = parentMember.Parent;
                    if (
                        !Program.dataTemplateXT.UnderlyingType.Assembly.FullName.StartsWith("System.Windows,")
                        &&
                        parentObject.Type == Program.dataTemplateXT)
                    {
                        XamlDomObject dataTemplate = parentObject;
                        object dataType = dataTemplate.GetMemberValueOfTypeProperty("DataType");

                        //TODO: Resolve should work here! XamlDomObject bug?
                        //Type dataTypeAsType = dataTemplate.Resolve(dataType as string);

                        XamlType xamlType = null;
                        if (dataType != null)
                        {
                            xamlType = dataTemplate.GetXamlTypeFromTypeNameString(dataType);
                        }
                        else
                        {
                            if (!Messages.ContainsKey(DataTemplateDataTypeNeeded))
                            {
                                Messages.Add(DataTemplateDataTypeNeeded, dataTemplate);
                            }
                        }
                        return xamlType;
                    }
                    else
                    {
                        return FindTypeContextInDom(parentObject);
                    }
                }
            }
        }
        private Type _ResolvedPathType;
        public Type ResolvedPathType
        {
            get
            {
                if (_ResolvedPathType != null)
                {
                    return _ResolvedPathType;
                }
                XamlType typeContext = this.TypeContext;
                if (typeContext == null)
                {
                    //Can't figure out ResolvedPathType if typeContext is null.
                    return null;
                }
                Type typeToWalk = typeContext.UnderlyingType;
                if (typeToWalk == null)
                {
                    _TypeContext = Program.SchemaContext.GetXamlType(
                        XamlDomUtilities.GetXamlTypeNameFromLocalAssembly(TypeContext.PreferredXamlNamespace, TypeContext.Name));
                    typeToWalk = this.TypeContext.UnderlyingType;
                }
                PropertyInfo propInfo = null;
                string[] pathParts;
                if (this.Path != null)
                {
                    pathParts = this.Path.Split(new char[] { '.' });
                }
                else
                {
                    //path is non-existant
                    pathParts = new string[0];
                }

                XamlType typeQualifier = null;
                foreach (string pathPart in pathParts)
                {
                    string pathPartString = pathPart;
                    string bracketString = null;
                    if (pathPartString.EndsWith("]"))
                    {
                        int bracket = pathPart.IndexOf('[');
                        bracketString = pathPart.Substring(bracket + 1);
                        pathPartString = pathPart.Substring(0, bracket);
                    }

                    bool continueAfter = false;
                    if (pathPartString.StartsWith("("))
                    {
                        int colon = pathPartString.IndexOf(':');
                        string ns = colon < 0 ? "" : pathPartString.Substring(1, colon - 1);
                        string typeName = colon < 0 ? pathPartString.Substring(1) : pathPartString.Substring(colon + 1);

                        typeQualifier = Program.SchemaContext.GetXamlType(
                                new XamlTypeName(this.DomObject.GetNamespace(ns),
                                typeName)
                            );
                        if (typeQualifier == null)
                        {
                            typeQualifier = Program.SchemaContext.GetXamlType
                                (XamlDomUtilities.GetXamlTypeNameFromLocalAssembly(
                                this.DomObject.GetNamespace(ns),
                                typeName
                                ));
                        }
                        continueAfter = true;
                    }

                    if (typeQualifier != null && !continueAfter)
                    {
                        int closeParen = pathPartString.IndexOf(')');
                        string propName = pathPartString.Substring(0, closeParen);
                        XamlMember xamlmember = typeQualifier.GetMember(propName);
                        typeToWalk = xamlmember.Type.UnderlyingType;
                        typeQualifier = null;
                        continueAfter = true;
                    }

                    if (!continueAfter)
                    {
                        propInfo = typeToWalk.GetProperty(pathPartString);
                        if (propInfo == null)
                        {
                            string messageString = PathPartNotResolved + pathPartString;
                            if (!Messages.ContainsKey(messageString))
                            {
                                Messages.Add(messageString, DomObject);
                            }
                            typeToWalk = null;
                            _ResolvedPathType = typeToWalk;
                            return _ResolvedPathType;
                        }
                        else
                        {
                            if (propInfo.Name == "SelectedItem")
                            {

                                typeToWalk = NamedElementItemType;
                                typeToWalk = typeToWalk == null ? typeToWalk = propInfo.PropertyType : typeToWalk;
                            }
                            else
                            {
                                typeToWalk = propInfo.PropertyType;
                            }
                        }
                    }
                    if (bracketString != null)
                    {
                        //TODO: handle [0][0]
                        PropertyInfo itemPropInfo = typeToWalk.GetProperty("Item");
                        typeToWalk = itemPropInfo.PropertyType;
                    }
                }

                if (typeToWalk != null)
                {
                    //now confirm that the target property is the same as the typeToWalk
                    if (typeToWalk.Name.StartsWith("Nullable`1"))
                    {
                        Type[] typeArgs = typeToWalk.GetGenericArguments();
                        typeToWalk = typeArgs[0];
                    }
                }
                _ResolvedPathType = typeToWalk;
                return _ResolvedPathType;
            }
        }

        public bool CanAssignToTargetProperty()
        {
            //if Binding can be assigned to the property directly, then say yes, it is assignable.
            if (Program.bindingXamlType.CanAssignTo(TargetProperty.Type))
            {
                return true;
            }

            bool typesMatch = false;

            Type typeToWalk = ResolvedPathType;

            if (TargetProperty == XamlLanguage.Items)
            {
                XamlType collectionType = DomObject.Parent.Parent.Type;
                Type collectionClrType = collectionType.UnderlyingType;
                Type[] typeArgs = collectionClrType.GetGenericArguments();
                if (typeArgs[0] == Program.bindingXamlType.UnderlyingType.BaseType)
                {
                    //TODO: mostly valid...target is ok.  need to understand validity of source values, paths etc...
                    return true;
                }
            }
            if (TargetProperty.Type.UnderlyingType.IsAssignableFrom(typeToWalk))
            {
                typesMatch = true;
            }
            else
            {
                if (typeToWalk != null)
                {
                    XamlType xamlTypeToWalk = Program.SchemaContext.GetXamlType(typeToWalk);
                    XamlValueConverter<System.ComponentModel.TypeConverter> converter
                        = xamlTypeToWalk.TypeConverter;
                    if (converter != null)
                    {
                        System.ComponentModel.TypeConverter typeConverter = converter.ConverterInstance;
                        if (typeConverter != null && typeConverter.CanConvertTo(TargetProperty.Type.UnderlyingType))
                        {
                            typesMatch = true;
                        }
                    }
                }
                if (!typesMatch)
                {
                    //TODO: handle binding.Converter
                    //TypeConversionAttribute() can be on the converter.
                    if (this.Converter != null)
                    {
                        if (this.Converter.GetType().Name == "StaticResourceExtension")
                        {
                            StaticResourceExtension sr = this.Converter as StaticResourceExtension;
                            XamlDomObject valueConverter = DomObject.FindResource(sr.ResourceKey);
                            Type valueConverterType = valueConverter.Type.UnderlyingType;
                            if (valueConverterType == null)
                            {
                                valueConverter.Type = Program.SchemaContext.GetXamlType(
                                    XamlDomUtilities.GetXamlTypeNameFromLocalAssembly(valueConverter.Type.PreferredXamlNamespace, valueConverter.Type.Name)
                                    );
                                valueConverterType = valueConverter.Type.UnderlyingType;
                            }
                            object[] attrs = valueConverterType.GetCustomAttributes(true);
                            bool foundValueConversionAttribute = false;
                            foreach (object attr in attrs)
                            {
                                Attribute attribute = attr as Attribute;
                                Type attrType = attr.GetType();
                                if (attrType.Name == "ValueConversionAttribute")
                                {
                                    foundValueConversionAttribute = true;
                                    PropertyInfo sourceTypeProp = attrType.GetProperty("SourceType");
                                    Type sourceType = (Type)sourceTypeProp.GetValue(attribute, null);
                                    PropertyInfo targetTypeProp = attrType.GetProperty("TargetType");
                                    Type targetType = (Type)targetTypeProp.GetValue(attribute, null);

                                    // does the converter do the right stuff
                                    if (TargetProperty.Type.UnderlyingType.IsAssignableFrom(targetType)
                                        && sourceType.IsAssignableFrom(typeToWalk))
                                    {
                                        typesMatch = true;
                                    }
                                    else
                                    {
                                        string messageString = PostConversionTypeMismatch;
                                        if (!Messages.ContainsKey(messageString))
                                        {
                                            Messages.Add(messageString, DomObject);
                                        }
                                    }
                                }
                            }
                            if (!foundValueConversionAttribute)
                            {
                                string messageString = ValueConversionAttributeNeeded;
                                if (!Messages.ContainsKey(messageString))
                                {
                                    Messages.Add(messageString, DomObject);
                                }
                            }
                        }
                    }
                }
            }
            return typesMatch;
        }

        public const string TypeMismatch = "Binding TypeMismatch possible. Assigning a value of type {1} to a property of type {0}";
        public const string ValueConversionAttributeNeeded = "Binding ValueConverter doesn't declare source and target types via ValueConversionAttribute";
        public const string PostConversionTypeMismatch = "Binding TypeMismatch, after conversion";
        public const string PathPartNotResolved = "Unable to resolve Binding.Path part: ";
        public const string DDesignContextNeeded = "TypeContext? d:DataContext need to provide typeContext to Bindings";
        public const string BindingSourceNotFound = "TypeContext? Binding.Source was not resolved";
        public const string RelativeSourceFindAncestorFailed = "TypeContext? RelativeSource.FindAncestor wasn't resolved";
        public const string RelativeSourceTemplatedParentFailed = "TypeContext? RelativeSource.TemplatedParent wasn't resolved";
        public const string ElementNameNotFound = "TypeContext? ElementName \"{0}\" wasn't resolved at compile time";
        public const string DataTemplateDataTypeNeeded = "TypeContext? DataTemplate.DataType needed to provide typeContext to Bindings";
    }
}
